package com.dynamic.mybatis.core.parser.xml;

import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.visitor.SchemaStatVisitor;
import com.alibaba.druid.stat.TableStat;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.dynamic.mybatis.core.enums.CommandType;
import com.dynamic.mybatis.core.enums.ReturnType;
import com.dynamic.mybatis.core.exceptions.DmsDocumentException;
import com.dynamic.mybatis.core.generator.MapperMethod;
import com.dynamic.mybatis.core.metadata.DynamicConfiguration;
import com.dynamic.mybatis.core.metadata.DynamicMappedStatement;
import com.dynamic.mybatis.core.mock.Mock;
import com.dynamic.mybatis.core.parser.NamespaceHelper;
import com.dynamic.mybatis.core.parser.field.DynamicMixedMappedStatementFieldParser;
import com.dynamic.mybatis.core.parser.xml.expand.MyDefaultElement;
import com.dynamic.mybatis.core.parser.xml.expand.MySAXReader;
import com.dynamic.mybatis.core.parser.xml.validator.DmsXMLMapperEntityResolver;
import com.dynamic.mybatis.core.parser.xml.validator.MybatisXmlValidator;
import com.dynamic.mybatis.core.session.DynamicSqlSession;
import com.dynamic.mybatis.core.utils.BeanUtils;
import com.dynamic.mybatis.core.utils.CollectionUtils;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.exceptions.ExceptionFactory;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xml.sax.SAXParseException;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @Description
 * @Author xs
 * @Date 2021/11/10 15:48
 */
public abstract class DynamicBaseMappedStatementParser {
    protected Configuration configuration;
    protected DynamicSqlSession dynamicSqlSession;
    protected static final String trimBlankRegex = "^\\s*|\\s*$";
    protected static final String[] statementXmlTags = "<select|<insert|<update|<delete|<parameterMap|<resultMap|<sql".split("\\|");

    protected static final String[] evalTags = Stream.of("update", "insert", "select", "delete", "reference").toArray(String[]::new);

    public DynamicBaseMappedStatementParser(DynamicSqlSession dynamicSqlSession) {
        this.dynamicSqlSession = dynamicSqlSession;
        this.configuration = dynamicSqlSession.getSqlSession().getConfiguration();
    }

    public void parseMapperXmlBodyAddConfiguration(String mapperBodyXml, String namespace) {
        parseMapperXmlAddConfiguration(MybatisXmlValidator.buildXmlMapperBody(mapperBodyXml, namespace));
    }

    public void parseMapperXmlAddConfiguration(String xml) {
        DynamicConfiguration dynamicConfiguration = (DynamicConfiguration) configuration;
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xml.getBytes());
        Map<String, XNode> sqlFragments = new HashMap<>();
        XMLMapperBuilder xmlMapperBuilder = createXMLMapperBuilder(byteArrayInputStream, configuration, dynamicConfiguration.DYNAMIC_RESOURCE, sqlFragments);
        xmlMapperBuilder.parse();
        dynamicConfiguration.addSqlFragments(sqlFragments);
    }

    public Set<String> setDynamicMappedStatementTables(DynamicMappedStatement dms, String namespace) {
        if (dms == null) {
            return Collections.emptySet();
        }
        Configuration configuration = dynamicSqlSession.getSqlSession().getConfiguration();
        //临时namespace
        String mappedStatementId = NamespaceHelper.getDynamicMappedStatementId(namespace, dms.getShortId());
        MappedStatement mappedStatement = !Objects.equals(dms.getId(), mappedStatementId) ? configuration.getMappedStatement(mappedStatementId) : null;
        //外部加载真实namespace
        if (mappedStatement == null) {
            mappedStatement = configuration.getMappedStatement(dms.getId());
        }

        Object parameter = Mock.mock(dms.getInputParameter().get(0));
        if(dms.isBatchExecute() && CollectionUtils.isNotEmpty(parameter)){
            parameter = ((Collection)parameter).toArray()[0];
        }else {
            parameter = dms.getOutputParameterMock();
        }

        String sql = mappedStatement.getSqlSource().getBoundSql(parameter).getSql();
        String urlSplit[];
        try (Connection connection = dynamicSqlSession.getConnection(dms.getId());) {
            urlSplit = connection.getMetaData().getURL().split(":");
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        List<SQLStatement> sqlStatements = SQLUtils.parseStatements(sql, urlSplit[1]);
        Collection<TableStat.Column> tableStatColumns = null;
        for (SQLStatement sqlStatement : sqlStatements) {
            SchemaStatVisitor visitor = SQLUtils.createSchemaStatVisitor(urlSplit[1]);
            sqlStatement.accept(visitor);
            tableStatColumns = visitor.getColumns();
        }
        Set<String> tables = tableStatColumns.stream().map(TableStat.Column::getTable).collect(Collectors.toSet());
        if (CollectionUtils.isNotEmpty(dms.getTables())) {
            tables.addAll(dms.getTables());
        }
        dms.setTables(tables);
        return tables;
    }

    protected List<ResultMap> parserResultMap(String id, String resultType, String resultMap) {
        List<ResultMap> resultMaps = new ArrayList<>();
        if (StringUtils.isNotBlank(resultType)) {
            Class resultTypeClass = configuration.getTypeAliasRegistry().resolveAlias(resultType);
            ResultMap resultMapObj = new ResultMap.Builder(configuration, id + "-Inline", resultTypeClass, new ArrayList<>(), null).build();
            resultMaps.add(resultMapObj);
        } else if (StringUtils.isNotBlank(resultMap)) {
            XNode xNode = new XPathParser(resultMap).evalNode("/resultMap");
            id = xNode.getStringAttribute("id");
            DynamicMappedStatement dms = new DynamicMappedStatement();
            dms.setId(id);
            dms.setSqlStatement(resultMap);
            parseDynamicMappedStatement(dms, true);
            resultMaps.add(configuration.getResultMap(id));
        }
        return resultMaps;
    }

    public DynamicMappedStatement parseDynamicMappedStatement(DynamicMappedStatement dms, boolean handler) {
        try {
            //解析动态新xml SQL
            if (isXml(trimBlank(dms.getSqlStatement()))) {
                DynamicConfiguration dynamicConfiguration = (DynamicConfiguration) configuration;
                String xml = MybatisXmlValidator.buildXmlMapperBody(dms.getSqlStatement(), dms.getDynamicMappedStatementNamespace());
                ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xml.getBytes());
                Map<String, XNode> sqlFragments = new HashMap<>();
                XMLMapperBuilder xmlMapperBuilder = createXMLMapperBuilder(byteArrayInputStream, configuration, dynamicConfiguration.DYNAMIC_RESOURCE, sqlFragments);
                xmlMapperBuilder.parse();
                dynamicConfiguration.addSqlFragments(sqlFragments);

                dynamicConfiguration.addDynamicMappedStatement(dms, handler);
            } else {
                //SQL解析
                SqlSource sqlSource = createSqlSource(configuration, trimBlank(dms.getSqlStatement()), null);
                //初始化mappedStatement
                MappedStatement.Builder builder = new MappedStatement.Builder(configuration, dms.getId(), sqlSource, CommandType.getSqlCommandType(dms.getCommandType()));
                builder.resultMaps(parserResultMap(dms.getId(), dms.getResultType(), dms.getResultMap()));
                MappedStatement mappedStatement = builder.build();
                //将mappedStatement 加入mybatis上下文中
                configuration.addMappedStatement(mappedStatement);

                ((DynamicConfiguration) configuration).addDynamicMappedStatement(dms, handler);
            }
            //解析字段
            DynamicMixedMappedStatementFieldParser fieldParser = dynamicSqlSession.getDynamicMappedStatementHelper().getMixedMappedStatementFieldParser();
            fieldParser.parseMappedStatementField(dms, null);

        } catch (Exception e) {
            throw ExceptionFactory.wrapException("mappedStatement parser is error or unknow type. mappedStatement is " + dms.getId() + " .Cause: " + e, e);
        }
        DynamicConfiguration dc = ((DynamicConfiguration) configuration);
        if (dms.getCommandType() == null) {
            MappedStatement mappedStatement = dc.getMappedStatement(dms.getId());
            dms.toBuilder().setCommandType(mappedStatement.getSqlCommandType());
        }
        if (!dc.hasMappedStatement(dms.getId())) {
            dc.addDynamicMappedStatement(dms, handler);
        }
        return dms;
    }


    public XMLMapperBuilder createXMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
        try {
            XPathParser xPathParser = new XPathParser(inputStream, true, configuration.getVariables(), new DmsXMLMapperEntityResolver());
            Constructor constructor = XMLMapperBuilder.class.getDeclaredConstructor(XPathParser.class, Configuration.class, String.class, Map.class);
            constructor.setAccessible(true);
            return (XMLMapperBuilder) constructor.newInstance(xPathParser, configuration, resource, sqlFragments);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        //  return  new XMLMapperBuilder(inputStream,configuration, resource, sqlFragments);
    }


    protected List<XNode> parser(String script) {
        List<XNode> xNodes = new ArrayList<>();
        for (String tag : statementXmlTags) {
            XNode xNode = createXPathParser(script).evalNode(tag);
            xNodes.add(xNode);
        }
        return xNodes;
    }

    public String validateXmlMapperBody(String xmlMapperBody, String namespace) throws SAXParseException {
        String xml = MybatisXmlValidator.buildXmlMapperBody(xmlMapperBody, namespace);
        MybatisXmlValidator.validate(xml, 3);
        return xml;
    }

    protected XPathParser createXPathParser(String xml) {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xml.getBytes());
        return new XPathParser(byteArrayInputStream, true, configuration.getVariables(), new DmsXMLMapperEntityResolver());
    }

    public List<DynamicMappedStatement> document4jParser(String xml) {
        Document document = createDom4jParser(xml);
        Element mapperElement = document.getRootElement();

        String namespace = mapperElement.attributeValue("namespace");
        List<Element> elements = evalNodes(mapperElement, evalTags);
        List<DynamicMappedStatement> dmsList = new ArrayList<>();
        DynamicConfiguration dynamicConfiguration = (DynamicConfiguration) configuration;
        for (Element element : elements) {
            String shortId = element.attributeValue("id");
            DynamicMappedStatement dms;
            if (element.getName().equals("reference")) {

                String referenceId = element.attributeValue("referenceId");
                DynamicMappedStatement referenceDms = dynamicConfiguration.getDynamicMappedStatement(referenceId);
                if (referenceDms == null) {
                    String message = " does not contain value for " + referenceId;
                    if (element instanceof MyDefaultElement) {
                        MyDefaultElement myDefaultElement = (MyDefaultElement) element;
                        throw new DmsDocumentException(message, myDefaultElement.getLineNumber(), myDefaultElement.getColumnNumber());
                    } else {
                        throw new IllegalArgumentException(message);
                    }
                }
                String dmsId;
                if (StringUtils.isBlank(shortId)) {
                    dms = referenceDms;
                    dmsId = NamespaceHelper.getDynamicMappedStatementId(namespace, dms.getShortId());
                } else {
                    dms = BeanUtils.serializeCopy(referenceDms);
                    dms = dms.toBuilder().setId(shortId).setNamespace(namespace).setReferenceId(referenceDms.getId()).builder();
                    dynamicConfiguration.addDynamicMappedStatement(dms, true);
                    dmsId = dms.getId();
                }
                MappedStatement ms = dynamicConfiguration.getMappedStatement(dmsId);
                if (ms == null) {
                    //生成新的MappedStatement
                    Element ele = getElementById(dms.getSqlStatement(), referenceDms.getShortId());
                    ele.attribute("id").setValue(dms.getShortId());
                    String elementXml = ele.asXML();
                    this.parseMapperXmlBodyAddConfiguration(elementXml, namespace);
                }

            } else {
                String id = NamespaceHelper.getDynamicMappedStatementId(namespace, shortId);
                ReturnType returnType = ReturnType.getReturnType(element.attributeValue("returnType"));
                MapperMethod mapperMethod = MapperMethod.get(shortId);
                returnType = returnType == null && mapperMethod != null ? ReturnType.getReturnType(mapperMethod.getReturnType().name()) : returnType;
                String method = CommandType.StandardCommand.SELECT.name().equalsIgnoreCase(element.getName()) ? "GET" : "POST";
                DynamicMappedStatement.Builder dmsBuilder = new DynamicMappedStatement().toBuilder()
                        .setId(id)
                        .setNamespace(NamespaceHelper.getNamespace(namespace))
                        .setSqlStatement(element.asXML())
                        .setCommandType(element.getName())
                        .setParameterWrapper(element.attributeValue("parameterWrapper"))
                        .setResultWrapper(element.attributeValue("resultWrapper"))
                        .setName(element.attributeValue("name"))
                        .setBatchExecute(Boolean.valueOf(element.attributeValue("batchExecute")))
                        .setBatchSize(Integer.valueOf(Objects.toString(element.attributeValue("batchSize"), "0")))
                        .setResultType(element.attributeValue("resultType"))
                        .setResultMap(element.attributeValue("resultMap"))
                        .setReturnType(returnType);
                dmsBuilder.setMethod(method);
                dms = dmsBuilder.builder();
            }
            dmsList.add(dms);
            mapperElement.remove(element);
            dms.setAuth(Boolean.TRUE);
        }
        return dmsList;
    }

    public Element parserDom4jMapperBodyXml(String mapperBodyXml, String namespace) {
        String xml = MybatisXmlValidator.buildXmlMapperBody(mapperBodyXml, namespace);
        return createDom4jParser(xml).getRootElement();
    }


    protected Document createDom4jParser(String xml) {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xml.getBytes());
        SAXReader saxReader = new MySAXReader();
        saxReader.setEntityResolver(new DmsXMLMapperEntityResolver());
        try {
            return saxReader.read(byteArrayInputStream);
        } catch (DocumentException e) {
            throw new DmsDocumentException(e);
        }
    }

    protected String evalIgnoreNodes(Element element) {
        return evalIgnoreNodes(element, evalTags);
    }

    protected String evalIgnoreNodes(Element element, String... ignoreTagNames) {
        List<Element> elements = evalNodes(element, ignoreTagNames);
        elements.stream().forEach(e -> element.remove(e));
        return Objects.toString(element.elements().stream().map(e -> ((Element) e).asXML()).collect(Collectors.joining(System.lineSeparator())), "");
    }

    protected List<Element> evalNodes(Element element, String... tagNames) {
        List<Element> elementList = element.elements();
        if (elementList == null) {
            return Collections.EMPTY_LIST;
        }
        List<String> tags = Arrays.stream(tagNames).collect(Collectors.toList());
        return elementList.stream().filter(e -> tags.contains(e.getName())).collect(Collectors.toList());
    }

    /**
     * @param configuration   mybatis上下文
     * @param mappedStatement 动态SQL
     * @param parameterType   参数类型
     *
     * @Description: 初始化sql对象
     */
    protected SqlSource createSqlSource(Configuration configuration, String mappedStatement, String parameterType) {
        Class parameterTypeClass;
        if (com.dynamic.mybatis.core.utils.StringUtils.isBlank(parameterType)) {
            parameterTypeClass = HashMap.class;
        } else {
            parameterTypeClass = configuration.getTypeAliasRegistry().resolveAlias(parameterType);
        }
        return configuration.getDefaultScriptingLanguageInstance().createSqlSource(configuration, mappedStatement, parameterTypeClass);
    }


    public Element getElementById(String mapperBodyXml, String id) {
        Element element = parserDom4jMapperBodyXml(mapperBodyXml, "test");
        for (Element e : (List<Element>) element.elements()) {
            if (Objects.equals(e.attributeValue("id"), id)) {
                return e;
            }
        }
        return null;
    }

    protected String trimBlank(String str) {
        if (str == null) {
            return str;
        }
        Pattern pattern = Pattern.compile(trimBlankRegex);
        Matcher matcher = pattern.matcher(str);
        return matcher.replaceAll("");
    }


    public static boolean isXml(String str) {
        for (String tag : statementXmlTags) {
            if (str.startsWith(tag)) {
                return true;
            }
        }
        return false;
    }
}